<!--
名称：完美（完整）表格对象，是表格组件TableComponent的plus版本。
包含一个模块查询、表格、添加、编辑、删除和批量删除等基本功能，建议常规模块使用，可以极大提高工作量，使得一个vue文件就能完成所有功能。
但是如果模块功能复杂，则建议还是分别使用查询组件、表格组件、对话框等组件来完成。
表格工具栏、分页插件、，并提供了多个插槽来解决各种定制需求
最后修改：20240817
作者：ljy
一、用法(略)
备注：可以参考角色模块、部门模块对本组件的使用方法。
-->
<template>
  <div>
    <!--查询组件-->
    <TableQueryComponent
      v-model:queryForm="queryForm"
      :loadTableList="loadTableList"
      :removeQueryField="removeQueryField"
      :clearQuery="clearQuery"
      :queryFormHeaders="queryFormHeaders"
      @beforeSubmitQuery="emit('beforeSubmitQuery', $event)"
      @beforeClearQuery="emit('beforeClearQuery')"
    >
      <!--查询表单左边的插槽-->
      <template #left>
        <slot name="query__left"> </slot>
      </template>
      <!--查询表单左边的插槽-->
      <template #right>
        <slot name="query__right"> </slot>
      </template>
      <!--自定义表格列插槽，在父组件自行进行创建-->
      <!-- 遍历表格的动态插槽 -->
      <template #[item.prop] v-for="(item, index) in queryFormHeaders" :key="index">
        <slot v-if="item.type === 'custom'" :name="'query_form_' + item.prop" v-bind:item="item"> </slot>
      </template>
    </TableQueryComponent>
    <!--表格对象-->
    <Table
      ref="tableRef"
      v-bind="$props"
      v-model:multipleSelection="multipleSelection"
      @addRow="addRow"
      @editRow="editRow"
      @rowClick="rowClick"
      @saveRow="saveTableRow"
      @afterDeleteSuccess="emit('afterDeleteSuccess', $event)"
    >
      <!--把表格左侧工具栏的右侧插槽继续往上暴露-->
      <template #toorbar_left_right>
        <slot name="toorbar_left_right"></slot>
      </template>
      <!--把表格右侧操作列的插槽继续往上暴露-->
      <template #table_operator="scope">
        <slot name="table_operator" v-bind:row="scope.row"></slot>
      </template>
      <!--把表格右侧操作列的左侧插槽继续往上暴露-->
      <template #table_operator_left="scope">
        <slot name="table_operator_left" v-bind:row="scope.row"></slot>
      </template>
      <!--把表格右侧操作列的右侧插槽继续往上暴露-->
      <template #table_operator_right="scope">
        <slot name="table_operator_right" v-bind:row="scope.row"></slot>
      </template>
      <!--自定义表格列插槽，在父组件自行进行创建-->
      <!-- 遍历表格的动态插槽 -->
      <template #[`table_column_`+item.prop]="scope" v-for="(item, index) in tableColumns" :key="index">
        <slot v-if="item.type === 'custom'" :name="'table_column_' + item.prop" v-bind:item="item" v-bind:row="scope.row"> </slot>
      </template>
    </Table>
    <!--表单对话框，一般用于添加或删除-->
    <FormDialogComponent
      ref="formDialogRef"
      v-bind="$props"
      v-model:dialogTitle="dialogTitle"
      v-model="form"
      :formHeader="formHeader"
      :formUrl="baseUrl"
      :formAction="formAction"
      @afterSubmitSuccess="afterSubmitSuccess"
      @afterFormSubmitFailure="emit('afterFormSubmitFailure')"
      @afterReset="afterReset"
      @beforeFormSubmit="emit('beforeFormSubmit', $event)"
      @dialogClosed="emit('dialogClosed')"
    >
      <!-- 遍历表单的动态插槽 -->
      <template #[`form_`+item.prop] v-for="(item, index) in formHeader" :key="index">
        <slot v-if="item.type === 'custom'" :name="'dialog_form_' + item.prop" v-bind:item="item" v-bind:formItem="form[item.prop]"> </slot>
      </template>
      <!--表单插槽底部，用于补充其他的插槽-->
      <template #form_bottom>
        <slot name="dialog_form_bottom"></slot>
      </template>
    </FormDialogComponent>
  </div>
</template>

<script lang="ts" setup>
// 导入表格查询组件
import TableQueryComponent from '@/components/base/table/TableQueryComponent.vue';
// 导入表格组件
import Table from '@/components/base/table/TableComponent.vue';
//导入表单对话框组件，用于管理模块的添加、编辑等功能
import FormDialogComponent from '@/components/base/table/form/FormDialogComponent.vue';
import { ElMessage, ElMessageBox, ElTable, formItemContextKey, type FormRules } from 'element-plus';
import { ref, type ModelRef, nextTick } from 'vue';
// 导入项目接口
import type { FormHeader, ResponseData, TableColumn, TableData } from '@/types/interface/base_interface';
// 导入字典
import { dictToMap } from '@/api/dict';
import Request from '@/api/request/request';

const emit = defineEmits([
  'beforeSubmitQuery',
  'beforeClearQuery',
  'rowClick',
  'beforeAddRow',
  'afterOpenDialogOfAddRow',
  'afterLoadDataSuccessInEditRow',
  'afterOpenDialogOfEditRow',
  'afterDeleteSuccess',
  'beforeFormSubmit',
  'afterSubmitSuccess',
  'afterSubmitSuccessOfAddRow',
  'afterSubmitSuccessOfEditRow',
  'afterFormSubmitFailure',
  'dialogClosed'
]);

// 接收父组件传递过来的值
interface Props {
  //函数，用于对应表格组件的Hook。需要在父组件引入自定义的@/api/uem/useTableList.ts组件，然后将其中的部分属性传过来
  loadTableList: () => void; // 当每页数据显示数发生变化时触发的一个函数
  changePageSize: (page: number) => void; // 当每页数据显示数发生变化时触发的一个函数
  changePage: (page: number) => void; // 当页面发生变化时触发的一个函数
  removeQueryField: (key: string) => void; // 移除查询条件中的某个元素
  clearQuery: () => void; // 当页面发生变化时触发的一个函数
  queryFormHeaders?: Array<FormHeader>; // 查询表单
  // query?: Record<string, any>; // 查询表单对象，初步设计是在导出表格时根据查询条件导出数据。
  //表格插件=================================================================
  baseUrl: string; // 表格的请求接口地址。如果后端接口符合规则，可以通过相同的url接口地址，以不同的提交方式实现增删改查
  tableHeight?: string | number; // 表格的默认高度，同element ui table的height属性。默认为自动
  table: TableData; // 表格对象，包含表格数据和分页信息。表格数据实际业务并不需要，偷懒写法
  tableColumns: TableColumn[]; // 表格的列字段集合
  isTreeTable?: Boolean; // 是否树形表格，如果为树形表格，会增加扩展和折叠两个按钮，默认为不是树形表格
  isExpandAll?: boolean; //树形表格是否默认展开
  showToolBar?: Boolean; // 是否显示表格工具栏，默认显示
  showLeftToolBar?: Boolean; // 是否显示左侧的表格工具栏，默认显示
  showRightToolBar?: Boolean; // 是否显示右侧的表格工具栏，默认显示
  showEditButtonInToolBar?: Boolean; // 是否显示工具栏的编辑按钮，默认不显示
  showAddNewButton?: Boolean; // 在表格工具栏是否显示新增按钮
  showDeleteMultipleSelectionButton?: Boolean; // 在表格工具栏是否显示删除按钮,还要配合是否有删除地址和表格复选框按钮是否存在判断
  showOperatorColumn?: Boolean; // 是否显示表格右侧的操作栏，默认显示
  showDeleteButtonInTable?: Boolean; // 在表格的每行记录都增加删除按钮
  showEditButtonInTable?: Boolean; // 在表格的每行记录都增加编辑按钮
  showDeleteButton?: Boolean; // 是否显示删除按钮，如果不显示则整个表格插件都不显示
  showEditButton?: Boolean; //  是否显示编辑按钮，如果不显示则整个表格插件都不显示
  AddNewButtonText?: string; //新增按钮的文本
  allowEditTableCell?: Boolean; //是否允许表格可以双击单元格编辑
  downloadTemplateUrl?: string; // 下载模板地址，也决定表格工具栏是否显示下载模板按钮
  templateFileName?: string; // 下载模板的文件名
  downloadUrl?: string; // 下载表格地址，也决定表格工具栏是否显示下载表格按钮
  dataFileName?: string; // 下载数据的文件名
  importUrl?: string; // 导入地址，决定是否显示导入按钮
  importButtonText?: string; // 导入按钮的名称
  importExcelSuccessAndCloseDialog?: Boolean; // 上传成功是否自动关闭对话框
  showTableCheckbox?: Boolean; // 是否在表格左侧显示复选框按钮,也决定工具栏删除按钮是否显示
  showTableIndex?: Boolean; // 是否在表格左侧显示序号
  rowId?: string; // 表格行对象的主键属性，需要根据后端接口数据来决定。默认是id
  operatorColumnWidth?: number; // 表格右侧操作了大小
  //表格组件内的分页组件
  pagination?: Boolean; // 是否显示分页插件，默认显示
  paginationLayout?: string; // 分页布局，同element 组件
  // 表单组件=================================================================
  // form属性在20220430修改为可选属性，意图避免在父组件初始化时，避免DIALOG初始化该属性报UNDEFINED
  // 虽然在父组件使用诸如const form = ref<Department>({});可以解决此问题，但是在typescript的Department接口中，如果包含了必须的属性，则该表单必须加上必须属性的值
  // 例如：const form = ref<Department>({ id: 1, name: '大数据技术'})，导致父组件应用中缺乏灵活性
  // form?: Record<string, any>; // 表单对象。
  //下面是基础对话框组件BaseDialog的基础属性（直接复制）
  // formType: T; //用于声明表单的类型，这里用泛型表示。注意必须要在setup中使用vue3.3版本提供的generic="T"
  dialogWidth?: string; // 对话框宽度
  // dialogTitle?: string; // 对话框标题,前台传入，不用加入添加或编辑的文本。添加或编辑的文本由action属性决定
  showDialogCancelButton?: Boolean; // 是否显示取消按钮
  dialogConfirmButtonText?: string; // 确认按钮上的文字
  showDialogFoot?: Boolean; // 是否显示对话框脚部
  dialogBodyStyle?: any; // 对话框主体内容的样式，要求以对象方式传入
  //下面是基础表单组件FormComponent的基础属性
  formHeader?: FormHeader[]; // 表单元素集合,用于生成表单
  formInline?: Boolean; // 表单是否未行内元素，对应form的inline属性。默认不是。注意如果是行内元素，建议在每个FormHeader中增加width描述
  formRules?: FormRules; // 整个表单的校检规则
  formLabelWidth?: string; // 表单宽度，对应el-form的label-width属性
  formLabelPosition?: string; // 表单的标签对齐方式，对应el-form的label-position属性（left、right、top）。
  showFormFoot?: Boolean; //是否显示表单底部，即两个默认的按钮。
  showFormResetButton?: Boolean; // 是否显示重置按钮
  clearFormValueAfterSubmitSuccess?: Boolean; //表单提交成功后是否清空值，ture表示清空，默认不清空。
  dataPickerSize?: 'default' | 'large' | 'small'; //日期输入框尺寸
}
const props = withDefaults(defineProps<Props>(), {
  // query: () => ({}), // 返回空对象,
  queryFormHeaders: () => [],
  formHeader: () => [],
  showFormFoot: () => Boolean(false) //默认不显示
});

// 对话框标题,前台传入，不用加入添加或编辑的文本。添加或编辑的文本由action属性决定
const dialogTitle = defineModel<string>('title', {
  required: false
});
// 查询组件表单对象。
const queryForm = defineModel<Record<string, any>>('query', {
  required: false,
  default: {}
});
//接收表单对象
const form: Record<string, any> = defineModel('form', {
  required: false
  // default: {}
});

// 复选框选择的记录row集合
const multipleSelection = defineModel<any[]>('multipleSelection', {
  required: false,
  default: []
});

// 定义表格的ref对象
const tableRef = ref<InstanceType<typeof ElTable>>();
//表单对话框对象
const formDialogRef = ref(); // 表单对话框的ref对象
// 表单对象，决定添加编辑表单的action。
const formAction = ref('post');
// 记录表单第一次传入的值,用于表单重置恢复初始值使用。
const formInit = ref<any>({});

//查询模块===========================================================================================

//表格模块===============================================================================================

//表格的刷新状态;
const tableLoading = ref(false); // 表格加载状态
const changeTableLoading = (loading: boolean) => {
  tableLoading.value = loading;
};

//表格行点击事件
const rowClick = (row: any, column: number): void => {
  emit('rowClick', row);
};
//表格行数据修改并且要保存。
//表示：row是表格变更后的行对象
const saveTableRow = async (row: any) => {
  const resData = await new Request(props.baseUrl, JSON.stringify(row)).put();
  if (resData.success) {
    //成功后是否需要整个表格都刷新有待观察，20250817提出
    ElMessage({
      message: `${dialogTitle.value}更新成功`,
      type: 'success'
    });
  } else {
    //如果更新失败
    await props.loadTableList(); //这里通过刷新获取原值
  }
};
/**
 * 主要适用于父组件手动打开表单，也可以是表单有特殊要求的操作。
 * 例如自定义的表单初始值、action。
 * action能够根据传入的值决定打开窗口的特性。action如果选择post则是表格组件默认的添加窗口，如果是Put或patch则是编辑窗口
 * @param cumtomAction 表单提交的动作，包含符合restful规范的get、post、put、patch等方式
 * @param customForm 表单初始值。如果该表单值实际是通过本组件的form传递，则可以省略。如tableRef.value.openDialog('post');
 */
const openDialog = async (cumtomAction: string, customForm: any) => {
  if (typeof customForm !== 'undefined') {
    //表单当前值等于表单值
    form.value = customForm;
  }
  //必须要加这个等dom更改渲染完在对初值进行改变
  await nextTick();
  //表单完成后重置表单的信息
  formInit.value = { ...form.value };
  formAction.value = cumtomAction;
  formDialogRef.value!.openDialog();
};
//当点击添加行。20241013最后修改
const addRow = async (): Promise<void> => {
  //再添加行以前有什么需要处理，例如传入初始值。但是注意该函数不能直接调用对话框里面子组件的函数
  await emit('beforeAddRow');
  //等待传入的值完成，必须要加
  await nextTick();
  //重新赋值表单初始值
  formInit.value = { ...form.value };
  formAction.value = 'post';
  //等待对话框加载完成，主要是等待对话框内的一些特定子组件加载完成。方便后续的调用子组件函数的操作。
  await formDialogRef.value!.openDialog();
  await nextTick();
  //如果需要直接调用对话框里面子组件的函数(一般是第一次打开对话框），则在这里处理
  await emit('afterOpenDialogOfAddRow');
  //等待传入的值完成，必须要加
  await nextTick();
  //重新赋值表单初始值
  formInit.value = { ...form.value };
  await nextTick();
};

// 编辑按钮操作。20241013最后修改
const editRow = async (row: any): Promise<void> => {
  //从服务端读取该行的记录
  const resData: ResponseData = await new Request(`${props.baseUrl}/${row.id}`).get();
  if (resData.success) {
    form.value = resData.data;
    //赋值后要等待dom更新才能获取准确的值
    await nextTick();
    //在成功获取数据后回调，注意应该表单初始值以前
    emit('afterLoadDataSuccessInEditRow', form.value);
    await nextTick();
    //重新赋值表单初始值
    formInit.value = { ...form.value };
    await nextTick();
  }
  //如果需要调试加上这个，等待form.value的值完成更新。
  // await nextTick();
  formAction.value = 'put';
  await formDialogRef.value!.openDialog();
  await nextTick();
  //如果需要直接调用对话框里面子组件的函数(一般是第一次打开对话框），则在这里处理
  if (resData.success) {
    await emit('afterOpenDialogOfEditRow', form.value);
    await nextTick();
    //重新赋值表单初始值
    formInit.value = { ...form.value };
    await nextTick();
  }
};
// 返回当前选中的行
const getSelectionRows = (): any => tableRef.value!.getSelectionRows();

//表格复选框
// const multipleSelection = ref([]); // 复选框选择的记录row
// const handleSelectionChange = (selection: []) => {
//   // 当选择项发生变化时会触发该事件，这里用于获取选择的记录（多选）.value可以获取所有选择了的行记录row
//   multipleSelection.value = selection;
//   emit('selectionChange', multipleSelection.value);
// };

//表单模块===============================================================================================

// 当关闭对话框后会出发重置函数
const afterReset = async () => {
  //如果需要调试加上这个，等待form.value的值完成更新。
  // await nextTick();
  //添加或成功后重置表单的信息
  form.value = { ...formInit.value };
};
//表单提交成功后（关闭对话框前）的回调函数
const afterSubmitSuccess = async () => {
  if (formAction.value === 'post') {
    //如果是添加，因为添加默认是不关闭对话框的。所以让表单最新的值等于之前创建时候的初始值
    form.value = { ...formInit.value };
    await nextTick();
    //在添加成功后一般会重置表单，但是如果重置表单后需要单独处理一些业务则调用此函数。
    //例如添加库位时要调用仓库级联选择组件，如果存储的是库位id，仓库id因非关联属性，要通过级联重新获取
    await emit('afterSubmitSuccessOfAddRow');
  } else if (formAction.value === 'put' || formAction.value === 'patch') {
    //如果是编辑，让初始值等于成功提交后的表单信息
    formInit.value = { ...form.value };
    //用于处理一些额外的操作例如库位级联选择对话框中，如果存储的是库位id，那么仓库id要通过级联重新获取
    //用于处理一些额外的操作,例如单独调用子组件中的函数。目前是预留，还没有使用场景
    await emit('afterSubmitSuccessOfEditRow');
  }
  await emit('afterSubmitSuccess');
};

//查询模块===============================================================================================

const toggleRowSelection = (row: any, selected: boolean) => tableRef.value!.toggleRowSelection(row, selected);
//用于单选表格，设定某一行为选中行， 如果调用时不加参数，则会取消目前高亮行的选中状态。
const setCurrentRow = (row: any) => tableRef.value!.setCurrentRow(row);
//清空表格选中的复选框
const clearSelection = () => tableRef.value!.clearSelection();
/**
 * 清除表单指定字段的验证
 * 设计与20250419，主要针对表单元素是自定义组件的情况。
 * 例如自定义组件中也有表单，当表单验证失败时，这个子表单里面的验证元素也会全部失效
 */
const clearFieldValidation = (fieldName: string) => {
  //移除表单项的校验结果。
  formDialogRef.value!.clearFieldValidation(fieldName);
};
/**
 * 验证表单指定字段
 * 设计与20250419，主要针对表单元素是自定义组件的情况。
 * 例如自定义组件中也有表单，当表单验证失败时，这个子表单里面的验证元素也会全部失效。使用完后，重新启用验证。
 */
const fieldValidation = (fieldName: string) => {
  //移除表单项的校验结果。
  formDialogRef.value!.fieldValidation(fieldName);
};
defineExpose({
  getSelectionRows,
  toggleRowSelection,
  setCurrentRow,
  clearSelection,
  openDialog,
  clearFieldValidation,
  fieldValidation
});
</script>
<style lang="scss" scoped></style>
